-
Notifications
You must be signed in to change notification settings - Fork 308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds an array reference type #1440
base: master
Are you sure you want to change the base?
Conversation
Copyable-ArrayViews 😮 nice! |
Early on I'd been trying to avoid the "layout-compatible structs" approach because I was worried about maintainability. But once I realized we could have 0 breaking changes at the cost of a maintenance concern, I went back and changed it. It helps that as long as nobody touches the internals of |
Ok, for real this time: add array types (plural!) and change the entire library to use them. There is a lot to write about this PR; frankly, I could make a blog post about it, and maybe we should. Certainly, the changelog is going to need to be extensive. I guess this is the first cut at that. This PR still needs significant documentation work before it is ready to merge; however, the vast majority of the implementation is now complete. What's Changed?This PR introduces three new types:
|
Element Deref Safety | Read elements | Mutate elements | Read shape / strides | Mutate shape / strides / pointer, but not data |
---|---|---|---|---|
Safe | &ArrRef |
&mut ArrRef |
&LayoutRef |
&mut LayoutRef |
Unsafe | &RawRef |
&mut RawRef |
&LayoutRef |
&mut LayoutRef |
If users need to a) alter the underlying buffer size of an array (e.g., shrink, reserve, append, etc), b) split an array into multiple pieces, or c) move data, they will need to take ArrayBase
or ArrayView
.
Deref
and AsRef
Implementations
To connect these types, we use a series of Deref
and AsRef
implementations, captured as follows:
┌─────────┐
┌──┼ArrayBase│
│ └────┬────┘
│ │Deref when S: Data
AsRef │ │DerefMut when S: DataMut
when │ │
S: RawData │ ┌────▼────┐
│ │ArrayRef ┼───────┐
AsMut │ └────┬────┘ │
when │ │Deref │AsRef
S: RawDataMut │ │DerefMut │AsMut
│ │ │
│ ┌────▼────┐ │
├──►RawRef ◄───────┤
│ └────┬────┘ │
AsRef │ │ Deref │AsRef
AsMut │ │ DerefMut │AsMut
│ │ │
│ ┌────▼────┐ │
└──►LayoutRef◄───────┘
└─────────┘
The result of this is that ArrayBase<S: RawData, D>
, RawArrayView
, or RawArrayViewMut
can only access methods on RawRef
and LayoutRef
via the AsRef
trait.
Writing Functions on RawRef
and LayoutRef
Users who want to write functions on RawRef
and LayoutRef
should write their functions or traits on a generic T where T: AsRef<RawRef<A, D>>
(and analogously for LayoutRef
). It's a slightly more cumbersome way to write functions, but provides two major benefits:
- Users can call the function directly, instead of having to call
.as_ref()
at every call site - If a function mutably take these types directly and is called on a shared array (without the callee using
.as_ref()
), the shared array will go through the dereferencing chain down to these types. As it passes throughArrayRef
it will guarantee unique access to the data, a potentially costly operation to ensure an invariant that is not guaranteed onRawRef
orLayoutRef
. By writing the generic: AsRef
implementation as described, codebases mitigate this computational footgun.
Future Designs
This limitations imposed on interactions with raw views, etc, can be fixed if the Trait specialization RFC lands.
The reference type, `RefBase<A, D, R>`, is parameterized by its element type, a dimensionality type, and its "referent" type. This last type is only a `PhantomData` marker, and acts to differentiate references that are "safe" from those that are "raw", i.e., references that come from an array whose storage is `Data` vs an array whose stoarge is `RawData`. The `DerefMut` implementation contains a check of uniqueness, guaranteeing that an `&mut RefBase` is safe to mutate. This comes with one breaking change and one important internal library note. The breaking change is that `ArrayBase` no longer implements `Copy` for views. This is because a `RefBase` does not know where it is pointing, and therefore cannot implement `Copy`; this, in turn, prohibits `ArrayBase` from implementing `Copy`. Users will now have to explicitly invoke `Clone` in the same way that they do for non-view arrays. The important library note has to do with the `.try_ensure_unique` check that occurs in the `DerefMut` implementation. The library's methods are allowed to *temporarily* break the invariant that an array's `ptr` is contained within `data`. The issue comes when trying to then alter `ptr`, `shape`, or `dim` while that invariant is broken. Code was using `self.ptr = ...` (where `self` is `ArrayBase`), which triggers `DerefMut`, which calls `try_ensure_unique`, which panics on the pointer inbounds check. As a result, code that is altering `RefBase` fields while the array's invariants are broken must be sure to write `self.aref.ptr = ...` instead.
…to RefBase and LayoutRef
…ow to write functionality with the new types
Hm, what does this mean "Maintainers of ndarray must guarantee going forward that LayoutRef never leaks its underlying data." Avoiding memory leaks in general is hard and not part of memory safety, but I'm not sure what this means, looking around in the PR to try to find out. You're getting very far ahead - that's exciting, but this is a lot to take in one bite or one PR! Plan for new types - AsRef vs Deref - backwards compat situation - should this be discussed? Why is AsRef needed? From the diagram it looks like deref coercion should work. I.e &ArrayRef will coerce to &LayoutRef, I thought. |
I know this is a big PR, and I'd be happy to break it up into smaller chunks that can be more easily reviewed and discussed! I did all of it because I kept running into design problems when I went to actually implement functionality. I think I've settled onto a very defensible design, but we should discuss and potentially break up the PR now that I feel more confident about the choices. Leak is the wrong word here, that's my bad. I meant "leak" in the sense of an API boundary: a On the note of backwards compatibility: I've tried very hard to design this such that it is fully backwards compatible. I think I achieved about 99% of that. Breaking this up into smaller PRs would let us identify where any incompatibilities may have snuck in. On the note of new types, while I'm fairly confident about the need for
Removing The The second reason is to imbue raw views with the functionality implemented on |
I believe this PR is now fully ready for review, with the exception of documenting
Or we can just leave it all as one. Let me know. Since this is such a big change, I'll also ping @nilgoyette, @adamreichold, @jturner314, and @termoshtt; I know everyone is busy, but I really want to avoid (even the perception of) unilaterally changing the library's internals without a larger maintainer consensus. Feel free to look, skim, criticize, question, etc. |
I'm impressed by this work. No tests even have to change, best possible backwards compatibility. Surely something must be a breaking change, some methods have been given new homes under different types (?) The diff is big but most of the changes are repetitive and can be skipped over, this is probably ok to review as it is. That's my initial feeling |
Thanks! While it's true that methods have new homes, the deref structure means that most things are callable from |
Let's push it to another PR. The serialize/deserialize code still works, since it uses the types' APIs instead of their underlying representations. Great coding by @dtolnay on that front. |
Done! I was missing a few. Also added documentation to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A big "Thank you" for taking the time and effort to do this! This is kinda crazy that it will be invisible for the users.
As I wrote earlier, I don't know much about the internals, so my review is mainly questions, not criticism.
src/alias_asref.rs
Outdated
/// Return an iterator over the length and stride of each axis. | ||
pub fn axes(&self) -> Axes<'_, D> | ||
{ | ||
<Self as AsRef<LayoutRef<_, _>>>::as_ref(self).axes() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand the logic, this saves work by using as ref?
Could we add an (initially private) helper method here - something that's the same as as_ref but without the types specified, so it would look like self.as_layout_ref().axes()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand the logic, this saves work by using as ref?
The AsRef
/ AsMut
aliasing functions serve two purposes. First off, a function like axes
that got moved to LayoutRef
would not longer be directly available on a raw view - i.e., raw_view.axes()
would be an error - because raw views do not dereference. (They would still exist on any array with S: Data
, though). So these aliases restore that capability to raw views.
The second is a performance consideration, only relevant for mutating functions on shared arrays. Functions defined on LayoutRef
and RawRef
are directly available to shared arrays, even without these aliases. However, they do that by going through &mut ArrayRef
, which un-shares the data. But we don't want that in these cases; LayoutRef
and RawRef
don't require unique data. So these aliases shadow these mutating functions and create a method that doesn't un-share data.
Could we add an (initially private) helper method here - something that's the same as as_ref but without the types specified, so it would look like self.as_layout_ref().axes()?
I've actually gone ahead and added those methods publicly - as you've pointed out, .as_ref
and .as_mut
, when used without type hints, are totally ambiguous. I think having explicit methods is helpful not just for us, but for our end users as well.
src/alias_asref.rs
Outdated
pub fn slice_collapse<I>(&mut self, info: I) | ||
where I: SliceArg<D> | ||
{ | ||
self.as_mut().slice_collapse(info); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My personal philosophy is that AsMut/AsRef
are conversion traits and should be used for argument conversion.
That philosophy would lead us to having more specific methods here - .as_layout_mut()
for example, and so on. The reason is among others that there are many AsMut implementations in the wild, and the number of them sometimes even grows. (Maybe that doesn't apply to our types, so maybe disregard this comment!) Any new AsRef/AsMut implementation - in a different crate - can lead to type inference failure in a new version.
I am mostly worried about that we would lead our users to use AsRef
and AsMut
in ways that are ultimately fragile for their code and for our API evolution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a great call. I've added the functions publicly (I think our users will want that stability and unambiguity as well) and replaced our usage in the crate.
I think that we should treat this change as worthy of the 'breaking-changes' label, unless we have very good evidence it's almost entirely free of existing code breakage.
|
I think I accidentally moved this over to ArrayRef, but we're not sure it should still require DataOwned
Ya, as much as I've done my best to avoid breaking changes, I think Rust's type inference will introduce errors, if nothing else. I agree we should likely treat this as a bump to 0.17. However, I think we could hold off on the release for a bit. We've got some other PRs we're trying to clean up, so we can roll those into a 0.17. As I've said on Zulip, I want to take a crack at the shape / stride problem, too. If I can make some headway, would that be good to roll into 0.17 as well? Or should we just aim for one major crate redesign per version?
|
This PR would add an array reference type into
ndarray
. This type's relationship toArrayBase
is analogous to the relationship between[T]
andVec<T>
. Closes #879; look there for more information on the motivation of this type.EDITED: See below
This implementation is chosen to make as few breaking changes as possible, introducing only one: array views are no longerCopy
. This is a break that the compiler will immediately error on, and can be fixed relatively easily with calls to.clone()
. As a result, the behavior is functionally equivalent, but the calling convention has to change. This change must happen because the concept of a reference type cannot implementClone
(since reference types must only be available via theDeref
traits as&RawRef
or&ArrRef
); as a result, it cannot beCopy
, and neither can any variant ofArrayBase
.I had originally tried to implementRawRef
andArrRef
as two different types, and it's worth discussing why I didn't. The goal there was to further reduce monomorphization bloat (the original motivation for the RFC mentioned above, although I don't think it's the most important reason) and to improve ergonomics of the reference type. However, this would fundamentally require breakingArrayBase
into two variants: raw views and everything else, since a single type cannot implementDeref
twice (trust me, I tried). Ifgeneric_const_exprs
orimplement_specialization
land, then this approach is possible and perhaps preferred.Right now, this PR just introduces the type and some documentation. It does not do the logical next step, which is moving most ofndarray
's functionality into theRefBase
type. This would accomplish the goal of creating a single way for users to write functions that accept arrays of all kinds. Neither does it add the functionality necessary forAddAssign
and its variants, which is also an important motivator for this change.